const ELEMENT_NODE = 1;//元素节点
const TEXT_NODE = 3;//文本节点
const TEXT = "TEXT";//文本类型
const CHANGE_TEXT = 'CHANGE_TEXT';//文本的内容改变
//依赖类 每1个属性都会对应创建一个依赖的实例
class Dep{
    constructor(){
        this.subs = [];//保存了观察者Watcher的数组
    }
    addSub(sub){//添加观察者
        this.subs.push(sub);
    }
    notify(){//通知观察者更新
        this.subs.forEach(sub=>sub.update());
    }
}
class Watcher{
    //观察vm上的expr属性，当vm上的expr属性发生变化的时候，执行cb
    constructor(vm,expr,cb){
        this.vm = vm;//Vue实例
        this.expr = expr;//Vue实例的属性名msg
        this.cb = cb;//回调函数
        Dep.target = this;//把当前Watcher实例赋给全局变量
        vm[expr];//尝试获取vm上的expr属性 vm.msg
        Dep.target = null;
    }
    update(){
        this.cb();
    }
}
class  VNode {
    constructor(type,attrs,children=[]){
        this.type = type;//节点的类型 div p span
        this.attrs = attrs;//节点的属性 id ,style,class
        this.children = children;//子节点 hello, []
    }
}
class Vue {
    //静态属性，保存着所有的全局的指令定义
    static directives = {};
    //静态方法，用来定义全局的指令
    static directive(name,definition){
        //保存指令定义属性是指令的名称，值是一个对象
        Vue.directives[name]=definition;
    }
    constructor(options) {
        //保存创建Vue实例的时候的选项对象
        this.$options = options;
        //保存数据对象
        this.$data = options.data;
        //初始化事件
        this.initEvents();
        //初始化生命周期
        this.initLifeCycle();
        //调用创建前的生命周期钩子
        this.callHook('beforeCreate');
        //初始化依赖注入系统
        this.initInjections();
        //初始化响应式系统
        this.initReactivity();
        //初始化计算属性
        this.initComputed();
        //初始化方法methods
        this.initMethods();
        //会在数据、计算属性、方法都初始化之后
        this.callHook('created');
        //如果你在选项中提供了el，则会立刻自动挂载节点
        if(options.el){
            this.$mount(options.el);
        }
    }
    initComputed(){
        console.log('initComputed')
    }
    initMethods(){
        console.log('initMethods')
    }
    $mount(selector){
        //获取要挂载的DOM元素
        let el = document.querySelector(selector);
        //获取模板内容,是一个字符串
        const template = this.$options.template?this.$options.template: el.outerHTML;
        //编译模板为render函数
        this.compile(template);
        //触发挂载前钩子
        this.callHook('beforeMount');
        //调用render方法获取虚拟DOM节点
        let vnode = this.vnode =  this.render();
        //根据虚拟DOM节点创建真实DOM节点 this.$el
        this.$el = this.createDOMElementFromVnode(vnode);
        //用新创建的DOM节点替换掉老的DOM节点
        el.parentNode.replaceChild(this.$el,el);
        this.callDirectiveHook(this.$el,'inserted');
        //触发挂载成功钩子函数
        this.callHook('mounted');
    }
    $forceUpdate(){
        this.callHook('beforeUpdate');
        //重新调用render方法，得到最新的虚拟DOM
        let newVnode = this.render();
        //创建一个补丁包
        let patch = [];
        //进行DOM-DIFF，收集DOM操作补丁
        this.domDiff(this.vnode,newVnode,patch);
        //比完之后，要让newVnode成为下一次比较时的老节点
        this.vnode=newVnode;
        //执行DOM操作补丁，修改真实DOM
        patch.forEach(({type,domElement,content})=>{
            //如果要修改文本内容的话
            if(type === CHANGE_TEXT){
                //把最新的文本内容赋值的真实DOM
                domElement.nodeValue = content;
            }
        });
        this.callHook('updated');
        this.callComponentUpdatedHooks(this.$el);
    }
    callComponentUpdatedHooks(rootNode){
        const elements = rootNode.querySelectorAll('*');
        elements.forEach((element)=>{
            this.callDirectiveHook(element,'componentUpdated');
        });
    }
    domDiff(oldVnode,newVnode,patch){
        //如果新老虚拟DOm类型一样，则表示老的DOM节点可以复用
        if(oldVnode.type === newVnode.type){
            //如果想复用老节点
            let domElement = newVnode.domElement=oldVnode.domElement;
            //如果新的老的节点都是文本节点的话
            if(oldVnode.type ===  TEXT){
                //如果文本节点的内容不一样的话
                if(oldVnode.content !== newVnode.content){
                    //向补丁包中放入一个DOM操作补丁
                    patch.push({
                        type:CHANGE_TEXT,//修改文本的内容
                        content:newVnode.content,//需要要修改成什么内容
                        domElement//修改哪个真实的文本节点
                    });
                }
            }else{//那就是元素节点，肯定会有儿子数组
                for(let i=0;i<oldVnode.children.length;i++){
                    //把每个儿子进行比较
                    this.domDiff(oldVnode.children[i],newVnode.children[i],patch)
                } 
            }
        }
    }
    createDOMElementFromVnode(vnode){
        //根据节点类型创建真实的DOM元素
        let domElement = document.createElement(vnode.type);
        //收集在当前的DOM节点定义的自定义指令
        let bindings = [];
        //给真实DOM元素添加属性
        for(let attrName in vnode.attrs){
            //如果v-开头，说明可能是自定义指令 attrName=v-draggable:absolute.ctr.disabled
            if(attrName.startsWith('v-')){
                const rawName = attrName;//自定义指令原始的属性名
                //获取指令的名称
                const name = attrName.slice(2).split(':')[0];//draggable
                //获取指令的参数 
                const arg = attrName.split(':')[1]?.split('.')[0];//absolute
                //获取指令的修饰符
                const modifiers = attrName.split(':')[1]?.split('.').slice(1).reduce((acc,modifier)=>{
                    acc[modifier]=true;
                    return acc;
                },{})
                //获取指令的定义对象
                const def = Vue.directives[name];
                if(def){
                    //获取值的原始的字符串表达式
                    const expression = vnode.attrs[attrName];//styleObj
                    bindings.push({
                        arg,
                        def,
                        expression,
                        modifiers,
                        name,
                        rawName,
                        value:this[expression]//this.styleObj
                    });
                }

            }else{//普通属性 id 
                domElement.setAttribute(attrName,vnode.attrs[attrName]);
            }
         
        }
        //bind钩子函数会在指令第一次绑定到元素时调用
        domElement.bindings=bindings;
        bindings.forEach((binding)=>{
            //如果这样写的话，其实跟DOM更新没有关系
             new Watcher(this,binding.expression,()=>{
                this.callDirectiveHook(domElement,'update');
             });
           });
           //执行此DOM元素上所有的自定义指令的bind钩子函数
           this.callDirectiveHook(domElement,'bind');
        //把每个儿子都变成真实DOM并且插入到父节点上
        vnode.children.forEach(childNode=>{
            if(childNode.type === TEXT){
                //如果虚拟DOM是文本节点的话,创建真实的文本节点
                let childDOMElement = document.createTextNode(childNode.content);
                //让虚拟DOM的domElement属性指向childDOMElement
                childNode.domElement=childDOMElement;
                //把此文本节点添加到父亲身上
                domElement.appendChild(childDOMElement);
            }else{
                //如果子节点是元素节点的话,创建子元素的DOM节点
                let childDOMElement = this.createDOMElementFromVnode(childNode);
                //把子节点添加到了父节点中
                domElement.appendChild(childDOMElement);
                //调用子节点DOm对象上自定义指令的inserted钩子
                this.callDirectiveHook(childDOMElement,'inserted')
            }
        });
        //让虚拟DOM的domElement属性指向它的真实DOM
        vnode.domElement = domElement;
        return domElement;
    }
    callDirectiveHook(domElement,hookName){
        domElement.bindings.forEach(binding=>{
            if(hookName ==='update' || hookName=== 'componentUpdated'){
                //先把当前的值保存到老值上
                binding.oldValue = binding.value;
                //重新计算获取最新的值赋给value
                binding.value=this[binding.expression];
            }
            binding.def[hookName]?.(domElement,binding);
        });
    }
    compile(template){
        //创建DOM解析器
        let domParser = new DOMParser();
        //使用DOM解析器解析模板字符串，转成文档对象
        let doc = domParser.parseFromString(template,'text/html');
        //获取根节点
        let rootNode = doc.body.firstChild;
        //根据根节点创建render函数，render执行的时候可创建虚拟DOM节点
        this.render = this.generateRenderFn(rootNode);
    }
    watchingKeys = new Set()
    generateRenderFn(node){
        //如果节点的类型是一个文本节点的话
        if(node.nodeType === TEXT_NODE){
            let text = node.nodeValue;//{{name}}
            let reg = /\{\{(.+?)\}\}/g;
            let match;
            //遍历匹配文本内容,如果找到了插件表达式 {{name}}
            while((match = reg.exec(text))!==null){
                const key = match[1];//name
                if(!this.watchingKeys.has(key)){
                    //创建一个观察，观察Vue实例上的name的变化，当发生变化后重新更新当前Vue实例
                    new Watcher(this,key,()=>this.$nextTick(this.$forceUpdate.bind(this)))
                    this.watchingKeys.add(key);
                }
            }
            return ()=>({
                type:"TEXT",//我们给文本节点定义一个类型，叫TEXT
                content:text.replace(reg,(_,key)=>this[key]),
                children:[]
            })
        //如果节点的类型是元素节点的话    
        }else if(node.nodeType === ELEMENT_NODE){
            //节点的标签名转小写就是类型   DIV=>div
            let type = node.tagName.toLowerCase();
            const attrs = Array.from(node.attributes).reduce((acc,attr)=>{
                acc[attr.name]=attr.value;///acc.id='app'
                return acc;
            },{});
            //获取每个子节点，把子节点也变成一个可以返回虚拟DOM的render函数
            const children = Array.from(node.childNodes).map(childNode=>this.generateRenderFn(childNode));
            return ()=>new VNode(type,attrs,children.map(childRender=>childRender()));
        }
    }
    initReactivity(){
        console.log(`initReactivity`);
        //数据代理，把$data上的属性代理给Vue实例this
        for(let key in this.$data){
            //获取data上的值
            let value = this.$data[key];
            let dep = new Dep();
            //为每个属性定义一个观察者的数组
            //给Vue实例也就是vm定义同名属性
            Object.defineProperty(this,key,{
                get(){
                    //如果当前是正在收集观察者的话，就把当前的观察者添加到依赖的观察者列表中
                    if(Dep.target){
                        dep.addSub(Dep.target);
                    }
                    return value;
                },
                set(newVal){
                    value = newVal;
                    //会通知此依赖的所有的观察者
                    dep.notify();
                }
            });
        }
    }
    initInjections(){
        console.log(`initInjections`)
    }
    //定义一个调用钩子函数的方法
    callHook(hookName){
        this.$options[hookName]?.call(this);
    }
    initLifeCycle(){
        console.log(`initLifeCycle`)
        //定义当前的Vue实例尚未挂载
        this._isMounted = false;
    }
    initEvents() {
        console.log(`initEvents`)
        //先初始化一下保存事件和监听的对象
        this._events = {};
    }
    //当调用Vue实例的on方法的时候，可以给某个事件添加监听函数
    $on(eventName, callback) {
        //先判断是事件没有对应的值,如果没有的话，说明没有添加过此事件的监听，则先赋为空数组
        if (!this._events[eventName]) {
            this._events[eventName] = [];
        }
        //向数组里添加一个新的监听函数
        this._events[eventName].push(callback);
    }
    $emit(eventName, ...args) {
        //获取此事件对应的监听函数数组
        const callbacks = this._events[eventName];
        if (callbacks) {
            //遍历执行所有的监听函数数组，记得透传参数
            callbacks.forEach(callback => callback(...args));
        }
    }
    //取消某个事件的监听
    $off(eventName, callback) {
        //获取此事件对应的回调函数的数组
        const callbacks = this._events[eventName];
        //如果有值的话
        if (callbacks) {
            if (typeof callback === 'function') {
                //获取要取消的监听函数在数组中的索引
                const index = callbacks.indexOf(callback);
                if (index !== -1) {
                    callbacks.splice(index, 1);
                }
            } else {
                this._events[eventName] = [];
            }
        }
    }
    callUnbindHooks(rootNode){
        const elements = rootNode.querySelectorAll('*');
        elements.forEach((element)=>{
            this.callDirectiveHook(element,'unbind');
        });
    }
    $destroy(){
        //准备开始销毁Vue实例
        this.callHook('beforeDestroy');
        //清除所有的事件监听函数
        this._events = {};
        //执行取消绑定的钩子函数
        this.callUnbindHooks(this.$el);
        //销毁后
        this.callHook('destroyed');
    }
    $nextTick(callback){
        setTimeout(callback,0);
    }
}